Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Coverage report
Test suite run success4032 tests passing in 1541 suites. Report generated by 🧪jest coverage report action from 7aae5d3 |
|
/snapit |
|
🫰✨ Thanks @dmerand! Your snapshot has been published to npm. Test the snapshot by installing your package globally: npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260327162821Caution After installing, validate the version by running |
e15c53b to
893af23
Compare
|
/snapit |
|
🫰✨ Thanks @dmerand! Your snapshot has been published to npm. Test the snapshot by installing your package globally: npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260327203223Caution After installing, validate the version by running |
97030c7 to
0a0c94d
Compare
3d0c271 to
c83e839
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new “store” workflow to Shopify CLI that can authenticate an app against a store (PKCE, per-user online tokens) and then execute Admin API GraphQL operations without requiring a local app project.
Changes:
- Introduces
shopify store auth(PKCE OAuth to loopback callback; persists online auth per store/user). - Introduces
shopify store execute(Admin API GraphQL execution with variable/query file support, mutation safety via--allow-mutations, and error/failure handling). - Adds supporting session storage, context/version resolution, transport helpers, and CLI docs/topic updates.
Reviewed changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/cli/src/index.ts | Registers store:auth and store:execute commands with the CLI entrypoint. |
| packages/cli/src/cli/commands/store/auth.ts | New shopify store auth command wiring/flags. |
| packages/cli/src/cli/commands/store/auth.test.ts | Tests flag parsing and service invocation for store auth. |
| packages/cli/src/cli/commands/store/execute.ts | New shopify store execute command wiring/flags. |
| packages/cli/src/cli/commands/store/execute.test.ts | Tests flag parsing and service invocation for store execute. |
| packages/cli/src/cli/services/store/auth-config.ts | Defines store auth client id, callback path/port, session keying, token masking. |
| packages/cli/src/cli/services/store/session.ts | Implements persisted per-store/per-user session bucket and expiry helper. |
| packages/cli/src/cli/services/store/session.test.ts | Unit tests for session bucket behavior and expiry margin logic. |
| packages/cli/src/cli/services/store/auth.ts | Implements PKCE flow, loopback callback server, token exchange, and persistence. |
| packages/cli/src/cli/services/store/auth.test.ts | Unit tests for PKCE helpers, callback server, token exchange, and persistence. |
| packages/cli/src/cli/services/store/execute-request.ts | Parses query/query-file, variables, validates single operation and mutation gating. |
| packages/cli/src/cli/services/store/execute-request.test.ts | Unit tests for request preparation and validation errors. |
| packages/cli/src/cli/services/store/admin-graphql-context.ts | Loads/refreshes stored auth and resolves Admin API version. |
| packages/cli/src/cli/services/store/admin-graphql-context.test.ts | Tests refresh flow, invalid auth paths, and version selection/validation. |
| packages/cli/src/cli/services/store/admin-graphql-transport.ts | Executes Admin GraphQL request and normalizes 401/GraphQL-error handling. |
| packages/cli/src/cli/services/store/admin-graphql-transport.test.ts | Tests success, 401 clearing + reauth message, GraphQL errors, and passthrough errors. |
| packages/cli/src/cli/services/store/graphql-targets.ts | Adds an internal target seam (currently Admin-only) for store-scoped GraphQL APIs. |
| packages/cli/src/cli/services/store/graphql-targets.test.ts | Tests target context preparation and execution delegation. |
| packages/cli/src/cli/services/store/execute-result.ts | Writes results to file or stdout and emits a success UI message. |
| packages/cli/src/cli/services/store/execute-result.test.ts | Tests file output vs stdout output behavior. |
| packages/cli/src/cli/services/store/execute.ts | Orchestrates request prep → context load → execution → result emission. |
| packages/cli/src/cli/services/store/execute.test.ts | End-to-end-ish unit tests for execute flow behaviors and error cases. |
| packages/cli/package.json | Adds a new store topic/category for CLI command grouping. |
| packages/cli/README.md | Adds docs entries for new commands (and includes regenerated usage sections). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/cli/README.md
Outdated
| -n, --name=<value> [env: SHOPIFY_FLAG_NAME] The name for the new app. When provided, skips the app | ||
| selection prompt and creates a new app with this name. | ||
| -p, --path=<value> [default: ., env: SHOPIFY_FLAG_PATH] | ||
| -p, --path=<value> [default: /Users/donald/src/github.com/Shopify/cli/packages/cli, env: |
There was a problem hiding this comment.
The generated docs include a machine-specific absolute path as the default for --path (/Users/donald/...). This is not portable and should not be committed; it should remain a generic default (e.g. .) or whatever the command’s real default is without embedding the generator’s working directory. Re-generate the README after fixing the docs generation/default handling so it doesn’t capture local filesystem paths.
| -p, --path=<value> [default: /Users/donald/src/github.com/Shopify/cli/packages/cli, env: | |
| -p, --path=<value> [default: ., env: |
| EXAMPLES | ||
| $ shopify store execute --store shop.myshopify.com --query "query { shop { name } }" | ||
|
|
||
| $ shopify store execute --store shop.myshopify.com --query-file ./operation.graphql --variables "{"id":"gid://shopify/Product/1"}" |
There was a problem hiding this comment.
This example’s JSON quoting will break in most shells because the inner double-quotes aren’t escaped. Update the README example to use proper quoting/escaping (e.g. single quotes around the JSON or escaped double-quotes) so it can be copy/pasted successfully.
| $ shopify store execute --store shop.myshopify.com --query-file ./operation.graphql --variables "{"id":"gid://shopify/Product/1"}" | |
| $ shopify store execute --store shop.myshopify.com --query-file ./operation.graphql --variables '{"id":"gid://shopify/Product/1"}' |
| const storedBucket = storage.get(storeAuthSessionKey(store)) | ||
| if (!storedBucket) return undefined | ||
|
|
||
| return storedBucket.sessionsByUserId[storedBucket.currentUserId] |
There was a problem hiding this comment.
getStoredStoreAppSession assumes the persisted bucket always has the expected shape. If the local storage entry is corrupted or from an older schema (e.g. missing sessionsByUserId/currentUserId), this will throw at runtime when running store execute. Consider validating the loaded value (type/field checks) and returning undefined (optionally deleting the bad key) instead of indexing into it unconditionally.
| const storedBucket = storage.get(storeAuthSessionKey(store)) | |
| if (!storedBucket) return undefined | |
| return storedBucket.sessionsByUserId[storedBucket.currentUserId] | |
| const key = storeAuthSessionKey(store) | |
| const storedBucket = storage.get(key) | |
| if (!storedBucket || typeof storedBucket !== 'object') return undefined | |
| const {sessionsByUserId, currentUserId} = storedBucket as Partial<StoredStoreAppSessionBucket> | |
| if ( | |
| !sessionsByUserId || | |
| typeof sessionsByUserId !== 'object' || | |
| typeof currentUserId !== 'string' | |
| ) { | |
| // Stored data is from an older schema or is corrupted; clear it and treat as no session. | |
| storage.delete(key) | |
| return undefined | |
| } | |
| const session = (sessionsByUserId as {[userId: string]: StoredStoreAppSession})[currentUserId] | |
| if (!session) { | |
| // Current user ID does not map to a stored session; clear invalid bucket. | |
| storage.delete(key) | |
| return undefined | |
| } | |
| return session |
|
|
||
| export function isSessionExpired(session: StoredStoreAppSession): boolean { | ||
| if (!session.expiresAt) return false | ||
| return new Date(session.expiresAt).getTime() - EXPIRY_MARGIN_MS < Date.now() |
There was a problem hiding this comment.
isSessionExpired treats an invalid expiresAt timestamp as “not expired” because new Date(expiresAt).getTime() becomes NaN and the comparison returns false. To avoid accidentally using a bad/unclear expiry value, treat invalid dates as expired (or clear the stored session) so the CLI refreshes/re-auths instead of proceeding with a likely-invalid token.
| return new Date(session.expiresAt).getTime() - EXPIRY_MARGIN_MS < Date.now() | |
| const expiresAtMs = new Date(session.expiresAt).getTime() | |
| if (Number.isNaN(expiresAtMs)) return true | |
| return expiresAtMs - EXPIRY_MARGIN_MS < Date.now() |
This comment has been minimized.
This comment has been minimized.
f079ab2 to
870584e
Compare
| @@ -0,0 +1,168 @@ | |||
| import {fetchApiVersions} from '@shopify/cli-kit/node/api/admin' | |||
There was a problem hiding this comment.
there are a lot of different concerns going on in this services area. it'd be nice to establish some clearer patterns of organization around things.
| outputContent`Refreshing expired token for ${outputToken.raw(session.store)} (expired at ${outputToken.raw(session.expiresAt ?? 'unknown')}, refresh_token=${outputToken.raw(maskToken(session.refreshToken))})`, | ||
| ) | ||
|
|
||
| const response = await fetch(endpoint, { |
There was a problem hiding this comment.
it'd be nice to split out the gql scaffolding from the specific queries/mutations
| } catch (error) { | ||
| if ( | ||
| error instanceof AbortError && | ||
| error.message.includes(`Error connecting to your store ${adminSession.storeFqdn}:`) && |
There was a problem hiding this comment.
can we add a code comment to address the string parsing later?
| [key: string]: StoredStoreAppSessionBucket | ||
| } | ||
|
|
||
| let _storeSessionStorage: LocalStorage<StoreSessionSchema> | undefined |
There was a problem hiding this comment.
ideally this gets initialized as part of some lifecycle and not inlined
| } | ||
|
|
||
| export function getStoreGraphQLTarget(api: StoreGraphQLApi): StoreGraphQLTarget<AdminStoreGraphQLContext> { | ||
| switch (api) { |
There was a problem hiding this comment.
what other targets are we going to be supporting?
| allowMutations: input.allowMutations, | ||
| }) | ||
|
|
||
| const context = await renderSingleTask({ |
There was a problem hiding this comment.
unrelated as it's an existing pattern, but i really want us to split out rendering and data flow more
ryancbahan
left a comment
There was a problem hiding this comment.
code generally makes sense. i think there are some improvements we can make to organization, and generally to establishing more clarity around what goes where.

What
Add
shopify store authandshopify store executeso Shopify CLI can authenticate an app against a store and then run Admin API GraphQL without needing a local app project in the current repo.shopify store authstarts a PKCE OAuth flow and stores online per-user auth for later store commands.shopify store executeruns Admin API GraphQL against that stored auth.--allow-mutations.shopify app executewhere it makes sense.Why
We want a clear store workflow in CLI:
This makes store operations usable outside an app project while still using standard app auth and explicit mutation safety.
How
shopify store authAdd a new
shopify store authcommand that:response_type=code,code_challenge,code_challenge_method=S256,code_verifier)127.0.0.1shopify store executeAdd a new
shopify store executecommand that:shopify app execute--allow-mutationsis passedSession handling
Store auth is persisted per store and per user for the configured app client ID.
When stored auth is expired,
shopify store executerefreshes it before version resolution / execution. When stored auth is no longer valid, the current stored session is cleared and the user is prompted to re-runshopify store auth.That invalid-auth path is handled consistently for:
Internal structure
The execute path is split into request preparation, auth/context loading, transport, and result emission helpers so the command stays thin and the execution flow is easier to reason about.
The implementation also adds a small internal target seam for future store-scoped GraphQL APIs while keeping the current behavior Admin-only.
Manual testing